001 - Fil
- Author: Kevin Traini
- Status: Published
- Publication date: 2024-07-11
- Last revision: 2025-01-02
- License: MIT
Abstract
Fil (thread, wire in French) is a programming language designed to be easy to use. It belongs mainly to functional and object-oriented paradigm. Its name is also a recursive acronym: Fil Is a Language (without the 'a' because it's ugly).
Table of Contents
1. Concept
Fil aims to be easy to use and totally not verbose. Syntax is as little as possible but complex enough to be able to do what you want. It belongs mainly to 2 paradigms (ordered by importance):
- Functional: everything is expression and can be used as a value for another expression.
- Object-oriented: data structures and encapsulation.
The language is strongly typed, this means there is no implicit conversion, you cannot assign an integer to a float.
1.1 Variables
It exists 2 ways to store values: variables and constant. The first is mutable but not the second.
Variables are pretty easy:
// One line declaration and definition
// The type is inferred by the compiler to i32
var my_variable = 2
// Two lines declaration then definition
// Note that you must declare its type as it cannot be inferred directly
var my_variable: i32
my_variable = 2
Constants are even easier:
// Constants can only be declared this way
// The type can be inferred by the compiler to f64
val my_const = 3.1415
1.2 Basic types
There is some basic types we can call scalar types.
Integer
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 |
u8 |
16-bit | i16 |
u16 |
32-bit | i32 |
u32 |
64-bit | i64 |
u64 |
128-bit | i128 |
u128 |
arch | int |
uint |
Float
There is f32
and f64
respectively for 32 and 64 bits floating point.
Boolean
The type is bool
and can either be true
or false
.
Character
To store character you can use the type char
. Under the hood it's an alias of the type u8
.
Arrays
Arrays are static by default, their size are determined at compile time from the declaration. To do so just add brackets
with the desired size (int[5]
). The size can be inferred by the compiler if you associate declaration with definition:
var my_array = [1, 2, 3] // Inferred to i32[3]
Note that this array size cannot be increased nor reduced, you can only change the contained values as long as
it's i32
.
You can also define pointer to these types by adding a *
just after (int*
for example). To access to the value
behind the pointer you can dereference it by putting a *
before the variable name (*my_var
for example). To retrieve
the address of a variable you can use &my_var
. To define a pointer you must use new
, for example new i32(5)
will
create a pointer to the value 5.
val my_pointer = new i32(5) // type is i32*
*my_pointer // 5
1.3 Basic operators
Numeric operators
They apply only on integers, floats and chars.
// Addition
1 + 2
1++
// Subtraction
6 - 5
3--
// Multiplication
2.0 * 0.5
// Division
6 / 3
// Remainder
42 % 3
Boolean operators
// Logical AND
true && false
// Logical OR
true || true
// Logical NOT
!false
Comparison operators
// Equality
1 == 1
// Inequality
2 != 3
// Lesser than
2 < 3
2 <= 2
// Greater than
3 > 2
3 >= 3
Array operators
Arrays are basically just pointers, so you can do pointer arithmetics (at your own risk). Else you can use array
accessor operator []
.
Assignation operators
All binary numeric and boolean operators can be used directly for assignation, for example:
var my_var = 2
my_var += 3
// my_var == 5
1.4 Conditions
First type of control flow expressions are conditions. There is if
and match
:
if
if (a == b) {
print("a equals to b")
} else if (a == c) {
print("a equals to c")
} else {
print("a has its own value")
}
match
match (my_var) {
"hello" -> print("Hello")
"world" -> {
print("Hello ")
print("World!")
}
_ -> print("Hi!") // Default case
}
The match expression can be very powerful, as you can match on array content or even data structure
match (my_array) {
[1, 2, 3] -> print("array is sorted")
_ -> print("array is not sorted")
}
match (someone) {
Person("John", "Doe") -> print("Hi John!")
Person(firstname, _) -> printf("Hi %s!", firstname)
}
1.5 Loops
To iterate there is classic for loops with an index variable, but there is also for loops which iterates directly on an array.
// Classic for loop
for (var i = 0; i < 10; i++) {
printf("Iteration %d", i)
}
// Loop on array
val my_array = [1, 2, 3]
for (val item: my_array) {
printf("Item is: %d", item)
}
As all other things, loops are expressions, so it means you can use them as generator for example.
val my_array = for (var i = 0; i < 10; i++) i
// Is equivalent to
val my_array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1.6 Functions
Functions are an important part of Fil, so they are easy to declare (note that declaration and definition cannot be separated).
fun sum(a: i32, b: i32): i32 {
printf("Do the sum of %d and %d", a, b)
a + b
}
This example function just do the sum of the two parameters, as you can see the return value of the function is the last expression of its body. For simple function like this one there is some shortest way to do it:
fun sum(a: i32, b: i32) (a + b)
// or
fun sum(a: i32, b: i32) = a + b
The return type of the function is not mandatory except for recursive function.
You can also store functions directly inside a variable and even pass them to a function. For that please use anonymous functions (or so-called lambdas). If you assign a classic function declaration to a variable, it will create an alias for this function.
val sum = (a: i32, b: i32) -> a + b
// The type of sum is: (i32, i32) -> i32
val result = sum(2, 3)
Parameters of functions can have default values (note that a parameter without default value cannot be after one with a default value).
fun mul(a: i32, b: i32 = 2) = a * b
And variadic parameters are also available. They must be the last one of the function. In that case the parameter can be used as an array.
from fil.algo use reduce // We'll see that later
fun sum(...operands: i32) = reduce(operands, (operand: i32, collector: i32) -> operand + collector, 0)
1.7 Data structures
Creating a data structure can be done in one line:
data Person
This structure is empty, but you can already use it.
var someone = Person()
You can add some properties to it on the same line.
from fil.str use String // We'll see that later
data Person(val firstname: String, val lastname: String)
var someone = Person("John", "Doe")
Let's talk a little more about properties. When you declare them between parenthesis just after the data structure name,
it also declares the default constructor of the structure. By declaring them with val
you tell that the property is
read-only. To be able to update it later use var
. By default, the property is public and so visible to anyone, you can
change this behavior by putting private
in front of the property.
This is data structures, so there is no inheritance, only composition. So you can do that:
// A simple data strucure
data Person(val firstname: String, val lastname: String)
// A composed data structure, there is no inheritence, only composition
data PersonWithAge(val person: Person, val birthdate: String)
// A union type. Someone is a Person xor a PersonWithAge
type Someone = Person | PersonWithAge
If you need to add some behavior to your data structure, you can attach it some members:
data Person(val firstname: String, val lastname: String) {
fun hello() = printf("Hi! I'm %s %s", this.firstname, this.lastname)
}
In example with composition, if structure PersonWithAge
also have the function hello
with the same signature, then
you can call this function directly on a variable of type Someone
.
Let's take the example of Complex
. Instead of adding function add
to the data structure, you can simply override
function operator+
:
data Complex(val real: f64, val imaginary: f64)
fun operator+(a: Complex, b: Complex) (
Complex(a.real + b.real, a.imaginary + b.imaginary)
)
Note that as declaration of a data structure is an expression, assigning it to a variable cause the variable to become an alias. It's the strange case where you can assign a type to a variable, this way you can also create aliases for all types.
val my_structure = data Person
val someone = my_structure()
1.8 Modules
Each Fil projects can be considered as module. The module name is a doted list of name (com.example.test
for example).
The standard library itself is a module.
A Fil module consist of a tree architecture of files and directories, the root can have a prefix and then each file will have a directive telling which part of the module it is:
mod my.module
When you divide your code into multiple files, each one will have its own module name (helping to avoid name collision). If you declare a function in one file, you can export it, and you use it in another file:
// Let's assume the project has my.module as prefix at the root directory
// sum.fil
mod my.module // Compiler consider the file as my.module.sum
export fun sum(a: i32, b: i32) = a + b
// main.fil
mod my.module // Compiler consider the file as my.module
from my.module.sum use sum
val result = sum(2, 3)
If you develop a library you can merge all your exports into one file and export them from it.
mod my.module
export from my.module.sum use sum
1.9 Genericity
Sometimes you would want to write a function which works with all types, but you cannot write alls by hand, it would be too long. For this case, there is genericity:
fun sum<T>(a: T, b: T) = a + b
This code define a function sum
which works with all types as long as they have override operator+
. Please note that
both parameters still have the same type.
The <>
just after the function name allow you to declare generic type name, it's a list of name, so you can declare as
many as you want. You can use it for functions, but also for data structure.
from fil.collection use Pair, Vector
data Map<K, V>: Vector<Pair<K, V>>
As you can see here, when using a data structure or a function with a generic type, you must tell which type you want to use. For a function call the type can be inferred.
val result = sum(2, 3) // T is inferred to i32
// The four versions are valid
val map = Map<i32, Person>()
val map: Map = Map<i32, Person>()
val map: Map<i32, Person> = Map()
val map: Map<i32, Person> = Map<i32, Person>()
2. Tools
2.1 filc
Fil compiler. It takes a list of files and compile them into binaries. It can do cross-compilation for other
architectures and also linkage to library developed in other languages (link to .so
, .o
, .a
, .dll
).
2.2 fmm
Fil module manager is a tool to help you use and share fil modules. It also manages your local fil environment by installing and updating both filc and standard library.
Through a config file, it can manage module dependencies, building, testing and publication to the registry.
2.3 Standard library
The standard library must as complete as possible. Users should find any basic utilities in it and even some complex one. Let's say that it must be as useful as c++ boost.
3. Development
All produced code must be open source under MIT license. The code must also be fully tested and available for as many platforms as possible.
Stable code is on master
branch. Features are developed on other branches (or fork) then merged into master through a
pull request. This pull request must be reviewed by at least the code owner, conversations resolved, tests all green.
Each commit must be attached to an issue and the pull request too.
License
MIT License
Copyright (c) 2024-Present Kevin Traini
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.